/*
 *  Licensed to the Apache Softwimport org.apache.commons.lang.StringUtils;

import fr.loof.fonzie.JPQLPersistenceQuery;
file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package fr.loof.fonzie.parsers;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;

import fr.loof.fonzie.PersistenceQuery;
import fr.loof.fonzie.QueryParser;
import fr.loof.fonzie.keywords.After;
import fr.loof.fonzie.keywords.AfterEquals;
import fr.loof.fonzie.keywords.Aggregate;
import fr.loof.fonzie.keywords.And;
import fr.loof.fonzie.keywords.Before;
import fr.loof.fonzie.keywords.BeforeEquals;
import fr.loof.fonzie.keywords.Between;
import fr.loof.fonzie.keywords.Desc;
import fr.loof.fonzie.keywords.Equals;
import fr.loof.fonzie.keywords.Fetch;
import fr.loof.fonzie.keywords.GreaterEquals;
import fr.loof.fonzie.keywords.GreaterThan;
import fr.loof.fonzie.keywords.GroupBy;
import fr.loof.fonzie.keywords.In;
import fr.loof.fonzie.keywords.JoinFetch;
import fr.loof.fonzie.keywords.LessEquals;
import fr.loof.fonzie.keywords.LessThan;
import fr.loof.fonzie.keywords.Like;
import fr.loof.fonzie.keywords.Not;
import fr.loof.fonzie.keywords.NotNull;
import fr.loof.fonzie.keywords.Null;
import fr.loof.fonzie.keywords.OfType;
import fr.loof.fonzie.keywords.OrderBy;

public class MethodNameQueryParser
    implements QueryParser
{
    private static Logger logger = LoggerFactory.getLogger( MethodNameQueryParser.class );

    @SuppressWarnings( "unchecked" )
    public PersistenceQuery parse( Class<?> entityClass, Aggregate aggregate, String criteria, Class<?>[] types )
    {
        logger.debug( "parse {} on {}", criteria, entityClass );

        List<QueryElement> elements = parse( entityClass, criteria );

        elements = postProcess( elements );

        JPQLPersistenceQuery jpqlQuery = new JPQLPersistenceQuery( entityClass, aggregate );
        List<Class> parameterTypes = new LinkedList<Class>( Arrays.asList( types ) );

        for ( QueryElement queryElement : elements )
        {
            queryElement.process( jpqlQuery, parameterTypes );
        }

        return jpqlQuery;
    }

    private List<QueryElement> parse( Class<?> entityClass, String criteria )
    {
        StringBuilder sb = new StringBuilder( criteria );
        List<QueryElement> elements = parseQueryElements( entityClass, sb );
        Assert.isTrue( sb.length() == 0, "unknown expression '" + sb + "' on query " + criteria );
        logger.debug( "query parsed as {} ", elements );
        return elements;
    }

    private List<QueryElement> postProcess( List<QueryElement> elements )
    {
        boolean updated;
        int pass = 1;
        do {
            updated = false;
            ListIterator<QueryElement> iterator = elements.listIterator();
            while ( iterator.hasNext() )
            {
                QueryElement element = iterator.next();
                if ( element instanceof QueryPostProcessor )
                {
                    updated |= ( (QueryPostProcessor) element ).postProcess( iterator );
                }
            }
            logger.debug( "{} pass post-precessed as {}", pass++, elements );
        }
        while ( updated );
        
        logger.debug( "query post-precessed as {}", elements );
        return elements;
    }

    protected List<QueryElement> parseQueryElements( Class<?> entityClass, StringBuilder sb )
    {
        List<QueryElement> query = new LinkedList<QueryElement>();
        while ( sb.length() > 0 )
        {
            Property path = parsePropertyPath( entityClass, sb );
            if ( path != null )
            {
                query.add( path );
            }
            KeyWord keyword = parseKeyWord( sb );
            if ( keyword != null )
            {
                query.add( keyword );
            }
            if ( path == null && keyword == null )
            {
                break;
            }
        }
        return query;
    }

    private List<KeyWord> keywords = new LinkedList<KeyWord>();
    {
        keywords.add( new Equals() );
        keywords.add( new GreaterThan() );
        keywords.add( new LessThan() );
        keywords.add( new GreaterEquals() );
        keywords.add( new LessEquals() );
        keywords.add( new Null() );
        keywords.add( new NotNull() );
        keywords.add( new Between() );
        keywords.add( new Before() );
        keywords.add( new BeforeEquals() );
        keywords.add( new After() );
        keywords.add( new AfterEquals() );
        keywords.add( new Like() );
        keywords.add( new In() );

        keywords.add( new Not() );
        keywords.add( new And() );
        keywords.add( new OrderBy() );
        keywords.add( new Desc() );
        // keywords.add( new Join() );
        keywords.add( new JoinFetch() );
        keywords.add( new Fetch() );
        keywords.add( new GroupBy() );
        keywords.add( new OfType() );

        Collections.sort( keywords );
    }

    protected KeyWord parseKeyWord( StringBuilder sb )
    {
        String s = sb.toString();
        for ( KeyWord keyWord : keywords )
        {
            if ( s.startsWith( keyWord.getKeyword() ) )
            {
                sb.delete( 0, keyWord.getKeyword().length() );
                return keyWord;
            }
        }
        return null;
    }

    protected Property parsePropertyPath( Class<?> type, StringBuilder path )
    {
        logger.debug( "check property path {} on type {}", path, type );
        Property property = null;
        for ( Field attribute : EntityMetaDataRepository.readAttributes( type ) )
        {
            String attrName = attribute.getName();
            String s = StringUtils.uncapitalize( path.toString() );
            if ( s.startsWith( attrName ) )
            {
                logger.trace( "attribute {} matches property path", attrName );
                path.delete( 0, attrName.length() );
                property = new Property( type, attrName, attribute.getType() );
                type = attribute.getType();
                if ( path.length() > 0 )
                {
                    Property next = parsePropertyPath( type, path );
                    if ( next == null )
                    {
                        break;
                    }
                    property.relativePath( next );
                }
                break;
            }
        }
        return property;
    }

}
